package fr.babos.touch.emulator 
{
	import flash.display.DisplayObject;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.TouchEvent;
	import flash.geom.Point;
	import flash.ui.Multitouch;
	import flash.ui.MultitouchInputMode;
	import flash.utils.Dictionary;
	import fr.babos.core.log.Log;
	import fr.babos.core.roles.IDisposable;
	import fr.babos.touch.delegate.IGestureTouchDelegate;
	import fr.babos.touch.errors.TouchError;
	import fr.babos.touch.events.GestureTouchDragEvent;
	import fr.babos.touch.events.GestureTouchEvent;
	import fr.babos.touch.events.GestureTouchSwipeEvent;
	import fr.babos.touch.events.GestureTouchTapEvent;
	import fr.babos.touch.events.GestureTouchTransformEvent;
	import fr.babos.utils.ArrayUtils;
	import fr.babos.utils.ScreenUtils;
	
	/**
	 * @author ZoulouX
	 */
	public class GestureTouchEmulator implements IDisposable
	{
		/**
		 * La liste des émulateurs attachés au containers
		 */
		protected static const __emulators				:Dictionary					= new Dictionary(false);
		
		
		/**
		 * La liste des émulateurs attachés au containers
		 */
		static public function get emulators ():Dictionary { return __emulators; }
		
		
		/**
		 * Emuler des GestureTouchEvent sur un container. Si ce container implémente IGestureTouchDelegate, les events seront mappés automatiquement sur gestureTouchHandler.
		 * @param	pTarget : Le container en question, peut implémenter IGestureTouchDelegate
		 * @param	pDelegate : Le delegate de cette emulation (optionnel).
		 * @return L'émulateur
		 */
		public static function emulate (pTarget:DisplayObject, pDelegate:IGestureTouchDelegate = null):GestureTouchEmulator
		{
			// Créer / retourner
			return new GestureTouchEmulator(pTarget, pDelegate);
		}
		
		/**
		 * Détacher un émulateur
		 * @param	pTarget : Le container sur lequel on veut arrêter l'émulation
		 */
		public static function demulate (pTarget:DisplayObject):void
		{
			// Si on a un émulateur sur ce container
			if (isEmulated(pTarget))
			{
				// On récupère
				var emulator:IDisposable = (__emulators[pTarget] as IDisposable);
				
				// On le supprime du dico
				__emulators[pTarget] = null;
				delete __emulators[pTarget];
				
				// On le dispose
				emulator.dispose();
			}
		}
		
		/**
		 * Si un container est émulé.
		 * @return True si ce container a un émulateur
		 */
		public static function isEmulated (pTarget:DisplayObject):Boolean
		{
			return __emulators[pTarget] != null;
		}
		
		
		
		/**
		 * Le container de cet emulateur
		 */
		protected var _rootTarget						:DisplayObject;
		
		/**
		 * Le stage du container
		 */
		protected var _rootStage						:Stage;
		
		/**
		 * Les ids des points
		 */
		protected var _pointsIds						:Array						= [];
		
		/**
		 * Les events
		 */
		protected var _events							:Array						= [];
		
		/**
		 * Le dernier event de déplacement
		 */
		protected var _dragEvent						:GestureTouchDragEvent;
		
		/**
		 * Le dernier event de tap
		 */
		protected var _tapEvent							:GestureTouchTapEvent;
		
		/**
		 * Le nombre de points en déplacement
		 */
		protected var _draggingPoints					:uint;
		
		/**
		 * Les points touch détéctés sur ce container
		 */
		protected var _positions						:Array						= [];
		
		/**
		 * Si les points ont bougés depuis le dernier dispatch
		 */
		protected var _hasMoved							:Array 						= [];
		
		/**
		 * Les directions
		 */
		protected var _directions						:Array						= [];
		
		/**
		 * Les deltas
		 */
		protected var _deltas							:Array						= [];
		
		/**
		 * Le nombre de points
		 */
		protected var _totalPoints						:uint						= 0;
		
		/**
		 * Une instance de point pour faire des calculs
		 */
		protected var _currentPoint						:Point 						= new Point();
		
		/**
		 * Le delegate de cet émulateur
		 */
		protected var _delegate							:IGestureTouchDelegate;
		
		/**
		 * Le multiplicateur pour la détéction de la direction
		 */
		protected var _directionDetectionMultiplier		:Number						= 2;
		
		/**
		 * Le ratio de ce stage
		 */
		protected var _stageRatio						:Number;
		
		/**
		 * Si on est en mode souris
		 */
		protected var _mouseMode						:Boolean					= false;
		
		
		
		/**
		 * Le delegate de cet émulateur
		 */
		public function get delegate ():IGestureTouchDelegate { return _delegate; }
		public function set delegate (value:IGestureTouchDelegate):void
		{
			// Enregistrer ce delegate
			_delegate = value;
		}
		
		/**
		 * Le multiplicateur pour la détéction de la direction
		 */
		public function get directionDetectionMultiplier ():Number { return _directionDetectionMultiplier; }
		public function set directionDetectionMultiplier (value:Number):void 
		{
			_directionDetectionMultiplier = value;
		}
		
		/**
		 * Le clip cible de l'émulateur
		 */
		public function get target ():DisplayObject { return _rootTarget;}
		
		/**
		 * Le nombre de points détéctés
		 */
		public function get totalPoints ():uint { return _totalPoints; }
		
		/**
		 * Les ID's des points détéctés
		 */
		public function get pointsIds ():Array { return _pointsIds; }
		
		
		/**
		 * Le constructeur
		 * @param	pTarget : Le container sur lequel on veut émuler les GestureTouchEvent. Peut implémenter IGestureTouchDelegate.
		 * @param	pDelegate : Le delegate de cette emulation (optionnel).
		 */
		public function GestureTouchEmulator (pTarget:DisplayObject, pDelegate:IGestureTouchDelegate = null)
		{
			// Vérifier la validité du container
			if (pTarget == null)
			{
				throw new TouchError("GestureTouchEmulator.construct", "pTarget can't be null in GestureTouchEmulator instanciation.");
				return;
			}
			
			// Désactiver l'ancien émulateur s'il y en avait un
			demulate(pTarget);
			
			// Ajouter ce nouvel émulateur au dico
			__emulators[pTarget] = this;
			
			// Enregistrer le container
			_rootTarget = pTarget;
			
			// Ecouter lorsque le target est supprimé
			_rootTarget.addEventListener(Event.REMOVED_FROM_STAGE, targetRemovedFromStage, false, 0, true);
			
			// Ecouter lorsque le target est ajouté
			if (_rootTarget.stage == null)
				_rootTarget.addEventListener(Event.ADDED_TO_STAGE, targetOnStageHandler, false, 0, true);
			else
				targetOnStageHandler();
			
			// Si on est en multitouch
			//if (Multitouch.inputMode == MultitouchInputMode.TOUCH_POINT)
			//{
				// Ecouter les touch events sur le container et son stage
				_rootTarget.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler);
			/*}
			else
			{
				// On est en mode souris
				_mouseMode = true;
				
				// Ecouter les mouse events
				_rootTarget.addEventListener(MouseEvent.MOUSE_DOWN, mouseBeginHandler);
			}*/
			
			// Si on a un delegate
			if (pDelegate != null)
			{
				// On l'enregistre
				delegate = pDelegate;
			}
			// Sinon vérifier si le container peut être un delegate
			else if (pTarget is IGestureTouchDelegate)
			{
				// On enregistre le container comme étant le delegate
				delegate = pTarget as IGestureTouchDelegate;
			}
		}
		
		/**
		 * Le target est sur le stage
		 */
		protected function targetOnStageHandler (event:Event = null):void
		{
			// Enregistrer le stage
			_rootStage = _rootTarget.stage;
			
			// Récuéprer le ratio de ce stage
			_stageRatio = ScreenUtils.getRatioForStage(_rootStage);
		}
		
		/**
		 * Un target a été supprimé
		 */
		protected function targetRemovedFromStage (event:Event):void 
		{
			demulate(event.currentTarget as DisplayObject);
		}
		
		/**
		 * Début d'un touch
		 */
		protected function touchBeginHandler (event:TouchEvent):void 
		{
			// Enregistrer ce point (event / position / delta)
			_events[event.touchPointID]				= event;
			_positions[event.touchPointID] 			= new Point(event.stageX / _stageRatio, event.stageY / _stageRatio);
			_deltas[event.touchPointID] 			= new Point(0, 0);
			_hasMoved[event.touchPointID]			= false;
			
			// Enregistrer l'id de ce point
			_pointsIds[_totalPoints] = event.touchPointID;
			
			// Compter le nombre de points
			// Si c'est le premier point ajouté
			if (++ _totalPoints == 1)
			{
				// Dispatcher l'annulation
				dispatchDragEvent(GestureTouchDragEvent.LOCK_DRAG);
				
				// On active l'écoute des moves et des ends
				_rootStage.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
				_rootStage.addEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler);
				_rootStage.addEventListener(TouchEvent.TOUCH_END, touchEndHandler);
			}
		}
		
		/**
		 * Début d'un mouse
		 */
		protected function mouseBeginHandler (event:MouseEvent):void
		{
			// Enregistrer ce point (event / position / delta)
			_positions[0] 	= new Point(_rootStage.mouseX / _stageRatio, _rootStage.mouseY / _stageRatio);
			_deltas[0] 		= new Point(0, 0);
			_hasMoved[0]	= false;
			
			// On a 1 point
			_totalPoints = 1;
			
			// Dispatcher l'annulation
			dispatchDragEvent(GestureTouchDragEvent.LOCK_DRAG);
			
			// On active l'écoute des moves et des ends
			_rootStage.addEventListener(Event.ENTER_FRAME, enterFrameForMouseHandler);
			_rootStage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
			_rootStage.addEventListener(MouseEvent.MOUSE_UP, mouseEndHandler);
		}
		
		/**
		 * Mouvements du curseur
		 */
		protected function mouseMoveHandler (event:MouseEvent):void
		{
			if (_positions == null)
			{
				Log.warning("GestureTouchEmulator.mouseMoveHandler problem.");
				return;
			}
			
			// La position globale du point
			_currentPoint.x = _rootStage.mouseX / _stageRatio;
			_currentPoint.y = _rootStage.mouseY / _stageRatio;
			
			// Calculer les deltas
			_deltas[0].x += _currentPoint.x - _positions[0].x;
			_deltas[0].y += _currentPoint.y - _positions[0].y;
			
			// Enregistrer la nouvelle position du point
			_positions[0].x = _currentPoint.x;
			_positions[0].y = _currentPoint.y;
			
			// Si ce point n'a pas de direction
			if (!(0 in _directions))
			{
				// Si on déplace significativement plus en X qu'en Y
				if (Math.abs(_deltas[0].x) > Math.abs(_deltas[0].y) * _directionDetectionMultiplier)
				{
					// On est en déplacement horizontal
					_directions[0] = 0;
				}
				
				// Si on déplace significativement plus en Y qu'en X
				else if (Math.abs(_deltas[0].y) > Math.abs(_deltas[0].x) * _directionDetectionMultiplier)
				{
					// On est en déplacement vertical
					_directions[0] = 1;
				}
				
				// Sinon
				else
				{
					// On est en déplacement libre
					_directions[0] = 2;
				}
				
				// On a un point de plus qui se déplace
				// On doit dispatcher un start drag si c'est le premier point détécté
				dispatchDragEvent(GestureTouchDragEvent.START_DRAG);
			}
			
			// Le point a bougé
			_hasMoved[0] = true;
		}
		
		/**
		 * Clic relâché
		 */
		protected function mouseEndHandler (event:MouseEvent):void
		{
			if (_positions == null)
			{
				Log.warning("GestureTouchEmulator.mouseEndHandler problem.");
				return;
			}
			
			// Purger les deltas
			enterFrameForMouseHandler();
			
			// On a 0 point
			_totalPoints = 0;
			
			// Si on avait une direction
			if (0 in _directions)
			{
				// On a un point de moins qui se déplace
				// Dispatcher l'arrêt du drag si on a plus de point en mouvement
				dispatchDragEvent(GestureTouchDragEvent.STOP_DRAG);
			}
			
			// On désactive l'écoute des moves et des ends
			_rootStage.removeEventListener(Event.ENTER_FRAME, enterFrameForMouseHandler);
			_rootStage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
			_rootStage.removeEventListener(MouseEvent.MOUSE_UP, mouseEndHandler);
			
			// Dispatcher la fin de l'annulation du drag
			dispatchDragEvent(GestureTouchDragEvent.UNLOCK_DRAG);
			
			// Si ce point n'avait pas bougé
			if (!(0 in _directions))
				dispatchTapEvent(GestureTouchTapEvent.SINGLE_TAP);
			
			// Supprimer le point
			delete _positions[0];
			
			// Supprimer le delta du point
			delete _deltas[0];
			
			// Supprimer si ce point a bougé
			delete _hasMoved[0];
			
			// Supprimer la direction du point
			delete _directions[0];
		}
		
		/**
		 * Boucle pour la souris
		 */
		protected function enterFrameForMouseHandler (e:Event = null):void
		{
			// Si ce point a une direction et une vitesse
			if (0 in _directions && _hasMoved[0])
			{
				// On dispatche le dragging
				dispatchDragEvent(GestureTouchDragEvent.DRAGGING);
				
				// Remettre ce point à 0
				_hasMoved[0] = false;
				_deltas[0].x = 0;
				_deltas[0].y = 0;
			}
		}
		
		/**
		 * Fin d'un touch
		 */
		protected function touchEndHandler (event:TouchEvent):void 
		{
			if (_positions == null)
			{
				Log.warning("GestureTouchEmulator.touchEndHandler problem.");
				return;
			}
			
			// Si ce point n'est pas dans la liste des points de cet émulateur
			if (!ArrayUtils.contains(_pointsIds, event.touchPointID))
			{
				// On quitte
				return;
			}
			
			// Purger les deltas
			enterFrameHandler();
			
			// Si on avait une direction
			if (event.touchPointID in _directions)
			{
				// On a un point de moins qui se déplace
				// Dispatcher l'arrêt du drag si on a plus de point en mouvement
				if (-- _draggingPoints == 0)
					dispatchDragEvent(GestureTouchDragEvent.STOP_DRAG);
			}
			
			// Si c'est le dernier point ajouté
			// Compter le nombre de points
			if (-- _totalPoints == 0)
			{
				// On désactive l'écoute des moves et des ends
				_rootStage.removeEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler);
				_rootStage.removeEventListener(Event.ENTER_FRAME, enterFrameHandler);
				_rootStage.removeEventListener(TouchEvent.TOUCH_END, touchEndHandler);
				
				// Dispatcher la fin de l'annulation du drag
				dispatchDragEvent(GestureTouchDragEvent.UNLOCK_DRAG);
				
				// Si ce point n'avait pas bougé
				if (!(event.touchPointID in _directions))
					dispatchTapEvent(GestureTouchTapEvent.SINGLE_TAP);
			}
			
			// Supprimer l'id de ce point
			_pointsIds = ArrayUtils.deleteElement(_pointsIds, event.touchPointID);
			
			// Supprimer les events
			delete _events[event.touchPointID];
			
			// Supprimer le point
			delete _positions[event.touchPointID];
			
			// Supprimer le delta du point
			delete _deltas[event.touchPointID];
			
			// Supprimer si ce point a bougé
			delete _hasMoved[event.touchPointID];
			
			// Supprimer la direction du point
			delete _directions[event.touchPointID];
		}
		
		/**
		 * Un point touch bouge
		 */
		protected function touchMoveHandler (event:TouchEvent):void 
		{
			if (_positions == null)
			{
				Log.warning("GestureTouchEmulator.touchMoveHandler problem.");
				return;
			}
			
			// Si ce point n'est pas dans la liste des points de cet émulateur
			if (!ArrayUtils.contains(_pointsIds, event.touchPointID))
			{
				// On quitte
				return;
			}
			
			// On calcule le deltas
			computeDeltas(event);
			
			// Si ce point n'a pas de direction
			if (!(event.touchPointID in _directions))
			{
				// Si on déplace significativement plus en X qu'en Y
				if (Math.abs(_deltas[event.touchPointID].x) > Math.abs(_deltas[event.touchPointID].y) * _directionDetectionMultiplier)
				{
					// On est en déplacement horizontal
					_directions[event.touchPointID] = 0;
				}
				
				// Si on déplace significativement plus en Y qu'en X
				else if (Math.abs(_deltas[event.touchPointID].y) > Math.abs(_deltas[event.touchPointID].x) * _directionDetectionMultiplier)
				{
					// On est en déplacement vertical
					_directions[event.touchPointID] = 1;
				}
				
				// Sinon
				else
				{
					// On est en déplacement libre
					_directions[event.touchPointID] = 2;
				}
				
				// On a un point de plus qui se déplace
				// On doit dispatcher un start drag si c'est le premier point détécté
				if (++ _draggingPoints == 1)
					dispatchDragEvent(GestureTouchDragEvent.START_DRAG);
			}
			
			// Enregistrer le nouvel event
			_events[event.touchPointID] = event;
			
			// Le point a bougé
			_hasMoved[event.touchPointID] = true;
		}
		
		/**
		 * Calculer les vitesses
		 */
		protected function computeDeltas (event:TouchEvent):void
		{
			// La position globale du point
			_currentPoint.x = event.stageX / _stageRatio;
			_currentPoint.y = event.stageY / _stageRatio;
			
			// Calculer les deltas
			_deltas[event.touchPointID].x += _currentPoint.x - _positions[event.touchPointID].x;
			_deltas[event.touchPointID].y += _currentPoint.y - _positions[event.touchPointID].y;
			
			// Enregistrer la nouvelle position du point
			_positions[event.touchPointID].x = _currentPoint.x;
			_positions[event.touchPointID].y = _currentPoint.y;
		}
		
		/**
		 * Boucle par frame pour les dispatchs de drag
		 */
		protected function enterFrameHandler (e:Event = null):void
		{
			// Parcourir les events
			for each (var event:TouchEvent in _events)
			{
				// Si ce point a une direction et une vitesse
				if (event.touchPointID in _directions && _hasMoved[event.touchPointID])
				{
					// On dispatche le dragging
					dispatchDragEvent(GestureTouchDragEvent.DRAGGING);
					
					// Remettre ce point à 0
					_hasMoved[event.touchPointID] = false;
					_deltas[event.touchPointID].x = 0;
					_deltas[event.touchPointID].y = 0;
				}
			}
		}
		
		/**
		 * Dispatcher un GestureTouchEvent
		 */
		protected function dispatchGestureTouchEvent (event:GestureTouchEvent):void
		{
			// Spécifier le target
			event.touchTarget = _rootTarget;
			
			// Si on n'a pas de delegate
			if (_delegate == null)
			{
				// Puis on dispatche sur la cible
				_rootTarget.dispatchEvent(event);
			}
			else
			{
				// On passe directe via le delegate
				_delegate.gestureTouchHandler(event);
			}
		}
		
		/**
		 * Dispatcher un event de Tap
		 * @param	pType : Le type d'event de tap
		 */
		protected function dispatchTapEvent (pType:String):void
		{
			// Créer l'event
			_tapEvent = new GestureTouchTapEvent(pType);
			
			// Dispatcher l'event sur le clip émulé
			dispatchGestureTouchEvent(_tapEvent);
		}
		
		/**
		 * Dispatcher un event de Drag
		 * @param	pType : Le type d'event de drag
		 */
		protected function dispatchDragEvent (pType:String):void
		{
			// TODO: Optimiser le dispatch (peut être éviter d'interpoler les points, etc)
			// TODO: Ajouter une propriété maxPoints pour limiter le nombre de points à gérer (et donc certaines gestures)
			
			// Créer l'event
			_dragEvent = new GestureTouchDragEvent(pType);
			
			// La direction du point
			if (_directions[_pointsIds[0]] == 0)
				_dragEvent.isHorizontal = true;
			else if (_directions[_pointsIds[0]] == 1)
				_dragEvent.isVertical = true;
			else
				_dragEvent.isBoth = true;
			
			// Si on a un seul point
			if (_totalPoints == 1)
			{
				// La position du seul point
				_dragEvent.position = _positions[_pointsIds[0]];
				
				// Le delta du seul point
				if (!_mouseMode)
				{
					_dragEvent.xDelta = _deltas[_pointsIds[0]].x;
					_dragEvent.yDelta = _deltas[_pointsIds[0]].y;
				}
				else
				{
					_dragEvent.xDelta = _deltas[0].x;
					_dragEvent.yDelta = _deltas[0].y;
				}
			}
			
			// Si on a deux points ou plus
			else if (_totalPoints > 1)
			{
				// La position interpolée des 2 points
				_dragEvent.position = Point.interpolate(
					_positions[_pointsIds[0]],
					_positions[_pointsIds[1]],
					.5
				);
				
				// Le delta interpolé des 2 points
				var deltaPoint:Point = Point.interpolate(
					_deltas[_pointsIds[0]],
					_deltas[_pointsIds[1]],
					.5
				);
				
				// Appliquer le delta sur l'event
				_dragEvent.xDelta = deltaPoint.x / 2;
				_dragEvent.yDelta = deltaPoint.y / 2;
			}
			
			// Ajouter tous les touchEvents a cet event
			_dragEvent.touchEvents = Vector.<TouchEvent>(_events);
			
			// Dispatcher l'event sur le clip émulé
			dispatchGestureTouchEvent(_dragEvent);
		}
		
		
		/**
		 * Supprimer proprement cet émulateur
		 */
		public function dispose ():void
		{
			// Ne plus écouter les events sur le container et son stage
			_rootTarget.removeEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler);
			_rootTarget.removeEventListener(Event.REMOVED_FROM_STAGE, targetRemovedFromStage);
			_rootStage.removeEventListener(TouchEvent.TOUCH_END, touchEndHandler);
			_rootStage.removeEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler);
			
			// Virer du dico si ce n'est pas déjà fait
			demulate(_rootTarget);
			
			// Supprimer les références
			_rootTarget = null;
			_rootStage = null;
			_positions = null;
			delegate = null;
			_dragEvent = null;
			_tapEvent = null;
		}
	}
}